﻿namespace Refunctor

open System
open System.Text
open Reflector
open Reflector.CodeModel
open Reflector.CodeModel.Memory
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Data
open System.Windows.Media
open System.Windows.Media.Imaging
open Microsoft.Win32
open System.IO
open ICSharpCode.AvalonEdit
open System.Windows.Controls.Primitives
open System.Diagnostics
open System.IO

[<Sealed>]
[<AllowNullLiteral>]
type internal ProgramEditor(serviceProvider : IServiceProvider) as self =
    inherit ContentControl()      
    
    [<DefaultValue>]    
    val mutable editor : ICSharpCode.AvalonEdit.TextEditor
        
    [<DefaultValue>]
    val mutable toolbar : ToolBar    

    let fileLabel = new Label(Margin = Thickness(100.0,0.,0.,0.), VerticalAlignment = VerticalAlignment.Center, FontSize = 14.)
    let outputWindow = new OutputWindow()

    let asmRefCollection = new ResizeArray<AsmRef>()

    [<DefaultValue>]
    val mutable setting : ProgramEditorOption
    
    let mutable sourceFileName = "FSharpProgram.fs"     
    let mutable outputFileName = "FSharpProgram.exe" 
    let mutable sourceFileFullName = String.Empty
    let mutable outputFileFullName = String.Empty
    let mutable isFileDirty = false

    let saveSourceFile() = 
        if not <| Directory.Exists self.setting.SourceFilePath then Directory.CreateDirectory self.setting.SourceFilePath |> ignore
        if not <| Directory.Exists self.setting.OutputPath then Directory.CreateDirectory self.setting.OutputPath |> ignore   
        File.WriteAllText(sourceFileFullName, self.editor.Text) 
                          
    do            
        self.InitializeComponents() 
        self.toolbar.Items.Add(fileLabel) |> ignore
        self.BindCommands()
        self.SetSetting(ProgramEditorOption.Load())
        saveSourceFile() (* As soon as Refunctor is loaded, empty source file is saved at default location. *)          
        fileLabel.Content <- sourceFileFullName
        self.editor.TextChanged.Add(fun _ -> isFileDirty <- true)

    member private self.InitializeComponents() =
        let toolItems : InvokerItemType<_> array =
            [| 
                CommandItem("New", "New (Ctrl+N)", ApplicationCommands.New)
                CommandItem("Open", "Open (Ctrl+O)", ApplicationCommands.Open)
                CommandItem("EditorSave", "Save (Ctrl+S)", ApplicationCommands.Save)
                CommandItem("Save As", "Save As", ApplicationCommands.SaveAs)                           
                InvokerItemType.Separator
                CommandItem("Cut", "Cut (Ctrl+X)", ApplicationCommands.Cut)
                CommandItem("Copy", "Copy (Ctrl+C)", ApplicationCommands.Copy)
                CommandItem("Paste", "Paste (Ctrl+V)", ApplicationCommands.Paste)
                CommandItem("Delete", "Delete (Del)", ApplicationCommands.Delete)
                InvokerItemType.Separator
                CommandItem("Undo", "Undo (Ctrl+Z)", ApplicationCommands.Undo)
                CommandItem("Redo", "Redo (Ctrl+Y)", ApplicationCommands.Redo)
                InvokerItemType.Separator
                CommandItem("Build", "Build (Ctrl+Shift+B)", RefunctorCommands.BuildCommand)
                CommandItem("Run", "Run (F5)", RefunctorCommands.RunCommand)  
                InvokerItemType.Separator
                CommandItem("Assembly References", "Assembly References", RefunctorCommands.AddAsmRefCommand)                
                CommandItem("Options", "Options", RefunctorCommands.OptionsCommand)           
            |] 
        let contextMenuItems : InvokerItemType<_> array =
            [|                
                CommandItem("Run", "F5", RefunctorCommands.RunCommand)
                CommandItem("Build", "Ctrl+Shift+B", RefunctorCommands.BuildCommand)      
                InvokerItemType.Separator
                CommandItem("Assembly References", String.Empty, RefunctorCommands.AddAsmRefCommand)                            
                InvokerItemType.Separator
                CommandItem("Cut", "Ctrl+X", ApplicationCommands.Cut)
                CommandItem("Copy", "Ctrl+C", ApplicationCommands.Copy)
                CommandItem("Paste", "Ctrl+V", ApplicationCommands.Paste)                
                InvokerItemType.Separator
                CommandItem("Undo", "Ctrl+Z", ApplicationCommands.Undo)
                CommandItem("Redo", "Ctrl+Y", ApplicationCommands.Redo)                                           
            |] 
        let grid = new Grid()
        grid.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))
        grid.RowDefinitions.Add(RowDefinition(Height = GridLength(1.,GridUnitType.Star)))
        grid.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))
                               
        self.toolbar <- buildToolbar toolItems        
        self.editor <- getEditor()
        self.editor.ContextMenu <- buildContextMenu contextMenuItems

        let border = new Border(BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1.7), Margin = Thickness(4.,0.,4.,0.), CornerRadius = CornerRadius(3.)) 
        Panel.SetZIndex(border,Int32.MaxValue)
        let editorGrid = new Grid()
        editorGrid.RowDefinitions.Add(RowDefinition(Height = GridLength(1.,GridUnitType.Star)))
        editorGrid.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))
        editorGrid.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))             
        Grid.SetRow(self.editor,0)
        editorGrid.Children.Add(self.editor) |> ignore
        Grid.SetRow(outputWindow,2)
        editorGrid.Children.Add(outputWindow) |> ignore
        (*  
        let splitter = new GridSplitter(HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, ResizeDirection = GridResizeDirection.Rows,
                                        Height = 1., Background = Brushes.DarkOliveGreen )
        Grid.SetRow(splitter,1)
        editorGrid.Children.Add(splitter) |> ignore
        *)
        border.Child <- editorGrid       
        Grid.SetRow(self.toolbar,0)
        Grid.SetRow(border,1)      
        grid.Children.Add(self.toolbar) |> ignore
        grid.Children.Add(border) |> ignore        
        self.Content <- grid
    
    member self.SetSetting(options : ProgramEditorOption ) =           
        self.setting <- options       
        self.editor.FontFamily <- getFont self.setting.EditorOption.FontName
        self.editor.FontSize <- self.setting.EditorOption.FontSize            
        self.editor.Options.IndentationSize <- int self.setting.EditorOption.IdentSize
        self.editor.WordWrap <- self.setting.EditorOption.WordWrap
        if self.setting.EditorOption.SyntaxHilighting then
            self.editor.SyntaxHighlighting <- Highlighting.HighlightingManager.Instance.GetDefinition("F#")
        else
            self.editor.SyntaxHighlighting <- null
        self.editor.Foreground <- new SolidColorBrush(self.setting.EditorOption.FontColor)                  
        self.editor.Background <- new SolidColorBrush(self.setting.EditorOption.BackgroundColor)          
        self.editor.ShowLineNumbers <- self.setting.EditorOption.ShowLineNumber
        self.editor.Options.ConvertTabsToSpaces <- self.setting.EditorOption.ConvertTabsToSpaces        
        self.editor.Options.ShowEndOfLine <- self.setting.EditorOption.ShowEndOfLine
        self.editor.Options.ShowSpaces <- self.setting.EditorOption.ShowSpace
        self.editor.Options.ShowTabs <- self.setting.EditorOption.ShowTabs
        self.setting.Save();
        sourceFileFullName <- Path.Combine(self.setting.SourceFilePath, sourceFileName)
        outputFileFullName <- Path.Combine(self.setting.OutputPath, outputFileName)

    let makeCommandBinding (command:#ICommand, execute :ExecutedRoutedEventHandler, canExecute : CanExecuteRoutedEventHandler) =
        self.CommandBindings.Add(new CommandBinding(command, execute, canExecute)) |> ignore
    
    let getOutputExt () =         
        let t = "--target:"
        let tstr = self.setting.FSCOptions.Split([|' '|], StringSplitOptions.RemoveEmptyEntries)
                        |> Array.tryPick (fun x -> if x.StartsWith(t) then Some(x) else None)
        let targetType = 
            match tstr with
            | Some(a) -> 
                match a.Split([|':'|], StringSplitOptions.RemoveEmptyEntries) with
                | [|a;b|] -> b
                | _ -> "exe"                
            | _ -> "exe"

        let outputExt =
            match targetType with
            | "module" -> ".netmodule"
            | "library" -> ".dll"
            | _ -> ".exe"
        outputExt

    let compile(runExe : bool) =        
        saveSourceFile()       
        outputFileFullName <- outputFileFullName.Substring(0, outputFileFullName.Length - 4 ) + getOutputExt()       
        let asmRef = String.Join(" ", asmRefCollection |> Seq.map (fun r -> "-r:\"" + r.Location + "\""))
        let args = "-o:\"" + outputFileFullName + "\" " + self.setting.FSCOptions + " " + asmRef + " \"" + sourceFileFullName + "\""               
        let info = new ProcessStartInfo();
        info.FileName <- self.setting.FSCPath
        info.RedirectStandardError <- true
        info.UseShellExecute <- false
        info.CreateNoWindow <- true
        info.Arguments <- args
        let fscProcess = new Process()
        fscProcess.StartInfo <- info                    
        fscProcess.Start() |> ignore
        let error = fscProcess.StandardError.ReadToEnd()
        let flag = fscProcess.ExitCode = 0
        let hideOutputWindow () = outputWindow.SetText String.Empty false(* hide output window *)
        if  flag && runExe then
            hideOutputWindow()
            let cmdInfo = new ProcessStartInfo()
            cmdInfo.FileName <- "cmd.exe"
            cmdInfo.Arguments <- String.Format("/k \"{0}\"",outputFileFullName)
            cmdInfo.WorkingDirectory <- self.setting.OutputPath
            Process.Start(cmdInfo) |> ignore            
        elif not flag then
            outputWindow.SetText error true(* show output window *)
        elif flag then
            hideOutputWindow()
        flag 
        
    member private self.BindCommands () =
        (* CommandBinding For OptionsCommand *)
        makeCommandBinding(RefunctorCommands.OptionsCommand,
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> 
                    let dialog = new ProgramPadOptionDialog(self.setting)   
                    dialog.ShowDialog() |> ignore
                    match dialog.DialogResult with
                    | DialogResult.OK -> 
                        self.SetSetting(dialog.Settings)                        
                    | DialogResult.Reset -> 
                        self.SetSetting(ProgramEditorOption.Default)                                          
                    | _ -> ()
                    ),                
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- true ))
                
        (* CommandBinding For Run *)
        makeCommandBinding(RefunctorCommands.RunCommand,
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> 
                    compile(true) |> ignore
                    ),                
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- self.editor.Text.Length > 0 ))  

        (* CommandBinding For BuildCommand *)
        makeCommandBinding(RefunctorCommands.BuildCommand,
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> 
                    if compile(false(*false means not to show the output*)) && self.setting.LoadToReflectorAfterBuild then                        
                        let assemblyManager =  serviceProvider.GetService(typeof<IAssemblyManager>) :?> IAssemblyManager
                        let outputAsm =  assemblyManager.Assemblies 
                                            |> Seq.cast 
                                            |> Seq.tryPick (fun (x:IAssembly) -> if x.Location =  outputFileFullName then Some(x) else None)                                              
                        let windowManager = serviceProvider.GetService(typeof<IWindowManager>) :?> IWindowManager                                                                                  
                        match outputAsm with
                        | Some(a) -> 
                            assemblyManager.Assemblies.Remove(a)                            
                            windowManager.Windows.[REFUNCTOR_WINDOW_ID].Visible <- true                            
                        | _ -> ()
                        
                        assemblyManager.LoadFile(outputFileFullName) |> ignore                        
                        let location = Path.GetFileNameWithoutExtension(outputFileName)
                        try
                            RemoteController.select (String.Format("code://{0}/{1}",location, location)) |> ignore                        
                        with
                        | _ -> () 
                        windowManager.Windows.["DisassemblerWindow"].Visible <- true
                   ),                
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- self.editor.Text.Length > 0 ))
        
        let save filePath = 
            try      
                use f = new FileStream(filePath, FileMode.Create)
                use writer = new StreamWriter(f)
                writer.Write(self.editor.Text)
                writer.Flush() 
                true                                                                                                     
            with
            | _ -> 
                showError("Sorry, there was an error saving the file.\nPlease try again.")
                false

        (* CommandBinding For SaveCommand *)
        makeCommandBinding
            (ApplicationCommands.Save,
            ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs ) -> 
                save sourceFileFullName |> ignore
                isFileDirty <- false                
            ),                                                                                                      
            CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs ) -> e.CanExecute <- true)) 
        
        let setPaths path =
            sourceFileFullName <- path
            outputFileName <- Path.GetFileNameWithoutExtension(path) + ".exe"
            outputFileFullName <- Path.Combine(self.setting.OutputPath, outputFileName)
            fileLabel.Content <- sourceFileFullName         

        let openFile () =
            let dialog = new OpenFileDialog()
            dialog.DefaultExt <- ".fs"
            dialog.Filter <- "FSharp File |*.fs"                 
            dialog.RestoreDirectory <- true
            dialog.Multiselect <- false
            let r = dialog.ShowDialog()
            if r.HasValue && r.Value  then
                self.editor.Text <- File.ReadAllText(dialog.FileName)  
                isFileDirty <- false
                setPaths dialog.FileName
        
        let addNew () =
            let dialog = new OpenNewFileDialog(self.setting.SourceFilePath)
            dialog.ShowDialog() |> ignore
            match dialog.DialogResult with
            | DialogResult.OK ->
                self.editor.Clear()  
                isFileDirty <- false              
                setPaths dialog.FullFileName
            | _ -> ()
        
        let askSavingWithCont cont =
            if isFileDirty then
                    let question = String.Format("Do you want to save changes to {0}?", Path.GetFileNameWithoutExtension(sourceFileFullName))
                    let r = askQuestion(question, "Refunctor")
                    match r with
                    | MessageBoxResult.Yes ->
                        save sourceFileFullName |> ignore
                        isFileDirty <- false
                        cont()
                    | MessageBoxResult.No -> cont()
                    | _ -> ()
            else cont()

        (* CommandBinding For OpenCommand *)                    
        makeCommandBinding
            (ApplicationCommands.Open,
            ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs ) ->  
                askSavingWithCont openFile                
            ),
            CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs ) -> e.CanExecute <- true))
        
        (* CommandBinding For SaveAsCommand *)
        makeCommandBinding
            (ApplicationCommands.SaveAs,
            ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs ) ->  
                let mutable flag = true
                let dialog = new SaveFileDialog()
                dialog.DefaultExt <- ".fs"
                dialog.Filter <- "FSharp File |*.fs" 
                dialog.FileName <- "FSharp"
                dialog.RestoreDirectory <- true                        
                let r = dialog.ShowDialog()
                if r.HasValue && r.Value then    
                    flag <- save dialog.FileName
                if flag then
                    isFileDirty <- false    
                    setPaths dialog.FileName
            ),
            CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs ) -> e.CanExecute <- true))
        
        (* CommandBinding For NewCommand *)
        makeCommandBinding
            (ApplicationCommands.New,
            ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs ) ->  
                askSavingWithCont addNew                                      
            ),
            CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs ) -> e.CanExecute <- true))

        (* CommandBinding For AddAsmRefCommand *)
        makeCommandBinding
            (RefunctorCommands.AddAsmRefCommand,
            ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs ) ->                  
                let dialog = new AsmRefDialog(asmRefCollection)                                  
                dialog.ShowDialog() |> ignore
                asmRefCollection.Clear()
                asmRefCollection.AddRange(dialog.AsmRefCollection)
            ),
            CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs ) -> e.CanExecute <- true))

    member self.AddAsmRef(asmRef : AsmRef) =
            if not <| asmRefCollection.Contains(asmRef) then
                asmRefCollection.Add(asmRef)